package de.sendorian.app.forumArchive.download;

import static org.apache.commons.lang.StringUtils.isEmpty;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang.time.DateFormatUtils;
import org.apache.log4j.Logger;
import org.hamcrest.Matcher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import de.sendorian.app.forumArchive.ReadmeCreator;
import de.sendorian.app.forumArchive.domain.Archive;
import de.sendorian.app.forumArchive.domain.Forum;
import de.sendorian.app.forumArchive.domain.Image;
import de.sendorian.app.forumArchive.domain.Link;
import de.sendorian.app.forumArchive.domain.Post;
import de.sendorian.app.forumArchive.domain.Topic;
import de.sendorian.app.forumArchive.matcher.FileBlackilistMatcher;
import de.sendorian.app.forumArchive.renamer.FileRenamer;
import de.sendorian.app.forumArchive.repository.FileRepository;
import de.sendorian.app.forumArchive.repository.ForumRepository;
import de.sendorian.util.io.FileWriter;

@Component
public class Downloader implements Runnable {

	private static final Logger LOG = Logger.getLogger(Downloader.class);

	FileRenamer renamer;
	Matcher<Post> postMatcher;
	Matcher<Topic> topicMatcher;
	private ReadmeCreator readmeCreator;
	private @Value("#{props['threads_download']}")
	int threads;
	private String downloadDir;

	private ExecutorService threadPool;
	private FileBlackilistMatcher linkBlacklist;

	private ForumRepository forumRepository;
	
	private FileRepository fileRepository;


	public Downloader() {
		linkBlacklist = new FileBlackilistMatcher(
				"file:external_links_ignores.properties");
	}


	@Override
	public void run() {
		downloadForums(forumRepository.findAll());
		forumRepository.shutdownDatabase();
	}


	public void downloadForums(List<Forum> forums) {
		for (Forum forum : forums) {
			downloadForum(forum);
		}
		readmeCreator.createReadme(forums);
	}


	public void downloadForum(Forum forum) {
		threadPool = Executors.newFixedThreadPool(threads);
		int imageCount = 0;
		int archiveCount = 0;

		Set<Link> externalLinks = new TreeSet<Link>();
		String downloadedFilesInfo = "";
		for (Topic topic : forum.getTopics()) {
			if (topicMatcher.matches(topic)) {
				TopicInfos topicInfo = doDownloadPosts(topic, forum.getName());
				if (!topicInfo.getDownloadedFiles().isEmpty()) {
					saveTextFile(topicInfo.getTopic().toStringFormatted(),
							topicInfo.getPath(), "Posts.txt");
					downloadedFilesInfo += topicInfo.getDownloadedFilesString();
					imageCount += topicInfo.getImageCount();
					archiveCount += topicInfo.getArchiveCount();
				}
				externalLinks.addAll(topicInfo.getLinks());
			}
		}

		String forumDir = downloadDir + forum.getName();
		String dateString = DateFormatUtils.format(new Date(), "yyyy_MM_dd");
		if (!isEmpty(downloadedFilesInfo)) {
			saveTextFile(downloadedFilesInfo, forumDir, "downloaded files "
					+ dateString + ".txt");
		}

		String externalLinksString = getExternalLinksString(externalLinks);
		if (!isEmpty(externalLinksString)) {
			saveTextFile(externalLinksString, forumDir, "external links "
					+ dateString + ".txt");
		}

		waitForDownloadsToComplete(forum.getName(), imageCount, archiveCount);
	}


	/**
	 * Downloads all Posts of the given topic that match the Downloaders post
	 * matcher.
	 */
	public TopicInfos downloadPosts(Topic topic, String forumName) {
		threadPool = Executors.newFixedThreadPool(threads);
		return doDownloadPosts(topic, forumName);
	}


	public TopicInfos doDownloadPosts(Topic topic, String forumName) {
		String forumPath = downloadDir + forumName;
		String path = forumPath + java.io.File.separator + topic.getName();
		Set<Link> links = new TreeSet<Link>();
		Set<String> downloadedFiles = new TreeSet<String>();
		int imageCount = 0;
		int archiveCount = 0;

		for (Post post : topic.getPosts()) {
			if (!postMatcher.matches(post)) {
				continue;
			}

			List<String> filesInArchives = new ArrayList<String>();
			if (!post.getArchives().isEmpty()) {
				for (Archive archive : post.getArchives()) {
					archive.setLocalPath(path);
					ArchiveDownloadTask archiveDownloader = new ArchiveDownloadTask(
							archive, topic, filesInArchives, fileRepository);
					threadPool.execute(archiveDownloader);
					downloadedFiles.add(archive.getName());
					archiveCount++;
				}
			}
			if (!post.getImages().isEmpty()) {
				for (Image image : post.getImages()) {
					// Renaming is for backwards compatibility only. Since
					// 1.0_beta3 the PostFinder will do this.
					// TODO delete when new database structure is final and
					// database is created this way
					renamer.rename(image);
					image.setLocalPath(path);

					if (!image.existsLocally()) {
						ImageDownloadTask imageDownloader = new ImageDownloadTask(
								image, topic, filesInArchives);
						threadPool.execute(imageDownloader);
						downloadedFiles.add(image.getName());
						imageCount++;
					} else {
						LOG.info("Skipping image " + image.getName()
								+ " from topic \"" + topic.getName()
								+ "\". It already exists.");
					}
				}
			}

			links.addAll(post.getLinks());
		}

		return new TopicInfos(topic, path, downloadedFiles, links, imageCount,
				archiveCount);
	}


	/**
	 * Waits until all download tasks have been executed.
	 */
	private void waitForDownloadsToComplete(String forumName, int imageCount,
			int archiveCount) {
		if (imageCount > 0 || archiveCount > 0) {
			LOG.info("For forum "
					+ forumName
					+ " "
					+ imageCount
					+ " image downloads and "
					+ archiveCount
					+ " archive downloads have been initiated. Waiting for tasks to complete.");
		} else {
			LOG.info("No download tasks for forum " + forumName + ".");
		}

		threadPool.shutdown();
		try {
			threadPool.awaitTermination(90 * 60, TimeUnit.SECONDS);

			if (imageCount > 0 || archiveCount > 0) {
				LOG.info("All download tasks for forum " + forumName
						+ " have been completed.");
			}
		} catch (InterruptedException e) {
			LOG.error("Downloader was interrupted!", e);
		}
	}


	/**
	 * Filters the links for external links, removes blacklisted links and sorts
	 * the entries. It then returns a String containing all these links, one per
	 * line.
	 * 
	 * @return a String containing one entry per line.
	 */
	public String getExternalLinksString(Set<Link> links) {
		Set<String> externalLinks = new TreeSet<String>();
		for (Link link : links) {
			if (link.isExternal() && !linkBlacklist.matches(link.getTarget())) {
				externalLinks.add(link.getTarget());
			}
		}
		return createTopicInfoList(externalLinks, null);
	}


	static String createTopicInfoList(Set<? extends Object> infosToSave,
			String title) {
		if (infosToSave.isEmpty())
			return "";

		StringBuilder builder = new StringBuilder();
		if (title != null) {
			builder.append(title + "\n");
		}
		for (Object info : infosToSave) {
			builder.append(info.toString()).append('\n');
		}
		builder.append("\n\n");
		return builder.toString();
	}


	private static void saveTextFile(String fileContent, String forumPath,
			String fileName) {
		try {
			FileWriter fileWriter = new FileWriter();
			fileWriter.deleteFile(forumPath, fileName);
			fileWriter.write(fileContent.getBytes(), forumPath, fileName);
		} catch (IOException e) {
			LOG.error("Could not write forum info file \"" + fileName + "\"", e);
		}
	}


	@Value("#{config.defaultRenamer}")
	public void setRenamer(FileRenamer renamer) {
		this.renamer = renamer;
	}


	@Value("#{config.postMatcher}")
	public void setPostMatcher(Matcher<Post> postMatcher) {
		this.postMatcher = postMatcher;
	}


	@Value("#{config.topicMatcher}")
	public void setTopicMatcher(Matcher<Topic> topicMatcher) {
		this.topicMatcher = topicMatcher;
	}


	@Autowired
	public void setForumRepository(ForumRepository forumRepository) {
		this.forumRepository = forumRepository;
	}
	
	@Autowired
    public void setFileRepository(FileRepository fileRepository) {
        this.fileRepository = fileRepository;
    }


	@Autowired
	public void setReadmeCreator(ReadmeCreator readmeCreator) {
		this.readmeCreator = readmeCreator;
	}

	
	@Value("#{config.downloadDir}")
	public void setDownloadDir(String downloadDir) {
		this.downloadDir = downloadDir;
	}
	
	public String getDownloadDir() {
		return downloadDir;
	}

}
